﻿namespace AjLogo.Compiler
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IO;

    using AjLogo.Language;
    using AjLogo.Primitives;

    public class Parser
    {
        private Lexer tokenizer;
        private Machine machine;

        public Parser(Machine machine, Lexer tokenizer)
        {
            if (machine == null)
                throw new ArgumentNullException("machine");

            if (tokenizer == null)
                throw new ArgumentNullException("tokenizer");

            this.machine = machine;
            this.tokenizer = tokenizer;
        }

        public Parser(string text)
            : this(new Machine(), new Lexer(text))
        {
        }

        public Parser(TextReader input)
            : this(new Machine(), new Lexer(input))
        {
        }

        public IExpression CompileExpression()
        {
            Token token = this.tokenizer.NextToken();

            while (token != null && token.TokenType == TokenType.EndOfLine)
                token = this.tokenizer.NextToken();

            this.tokenizer.PushToken(token);

            return this.CompileSimpleExpression();
        }

        public IExpression CompileSimpleExpression()
        {
            Token token = this.tokenizer.NextToken();

            if (token == null)
                return null;

            switch (token.TokenType)
            {
                case TokenType.Integer:
                    return new Constant(token.Value);

                case TokenType.QuotedName:
                    return new Quote((string)token.Value);

                case TokenType.DotsName:
                    return new Dots((string)token.Value);

                case TokenType.Name:
                    if (IsTo(token))
                    {
                        this.CompileTo();

                        // TODO review how to do this
                        return this.CompileExpression();
                    }

                    return this.CompileCall((string)token.Value);

                case TokenType.Separator:
                    if ("(".Equals(token.Value))
                    {
                        IExpression expr = this.CompileExpression();
                        this.CompileToken(")");
                        return expr;
                    }

                    if ("[".Equals(token.Value))
                    {
                        IExpression expr = new Constant(this.CompileListValue());
                        this.CompileToken("]");
                        return expr;
                    }

                    this.tokenizer.PushToken(token);

                    return null;

                case TokenType.EndOfLine:
                    return null;
            }

            throw new ParserException(string.Format(CultureInfo.CurrentCulture, "Unexpected {0}", token.Value.ToString()));
        }

        private static bool IsTo(Token token)
        {
            if (!(token.Value is string))
                return false;

            return ((string) token.Value).Equals("to", StringComparison.OrdinalIgnoreCase);
        }

        private static bool IsEnd(Token token)
        {
            if (token == null)
                return false;

            if (!(token.Value is string))
                return false;

            return ((string)token.Value).Equals("end", StringComparison.OrdinalIgnoreCase);
        }

        private ListValue CompileListValue()
        {
            ListValue list = null;
            ListValue lastItem = null;

            Token token = this.tokenizer.NextListToken();

            while (token != null && (string) token.Value != "]")
            {
                object value;

                if ((string) token.Value == "[")
                {
                    value = this.CompileListValue();
                    this.CompileToken("]");
                }
                else
                    value = token.Value;

                if (list == null)
                {
                    list = new ListValue(value, null);
                    lastItem = list;
                }
                else
                {
                    ListValue list2 = new ListValue(value, null);
                    lastItem.Rest = list2;
                    lastItem = list2;
                }

                token = this.tokenizer.NextListToken();
            }

            if (token != null)
                this.tokenizer.PushToken(token);

            return list;
        }

        private void CompileTo()
        {
            string name = this.CompileName();

            Collection<string> parnames = new Collection<string>();

            string parname = this.CompileParameter();

            while (parname != null)
            {
                parnames.Add(parname);
                parname = this.CompileParameter();
            }

            Token token = this.tokenizer.NextToken();

            IExpression expr;

            if (token != null && token.TokenType == TokenType.EndOfLine)
                expr = this.CompileMultiLineExpressions();
            else
            {
                this.tokenizer.PushToken(token);
                expr = this.CompileLineExpressions();
            }

            Procedure procedure = new Procedure(expr, parnames);

            this.machine.SetProcedure(name, procedure);
        }

        private IExpression CompileMultiLineExpressions()
        {
            ExpressionList list = new ExpressionList();

            Token token = this.tokenizer.NextToken();

            while (!IsEnd(token))
            {
                this.tokenizer.PushToken(token);

                list.Add(this.CompileExpression());

                token = this.tokenizer.NextToken();

                while (token != null && token.TokenType == TokenType.EndOfLine)
                    token = this.tokenizer.NextToken();
            }

            return list;
        }

        private IExpression CompileLineExpressions()
        {
            ExpressionList list = new ExpressionList();

            Token token = this.tokenizer.NextToken();

            while (token != null && token.TokenType != TokenType.EndOfLine)
            {
                this.tokenizer.PushToken(token);

                list.Add(this.CompileExpression());

                token = this.tokenizer.NextToken();
            }

            return list;
        }

        private string CompileName()
        {
            Token token = this.tokenizer.NextToken();

            if (token == null || token.TokenType != TokenType.Name)
                throw new ParserException("Expected name");

            return (string) token.Value;
        }

        private string CompileParameter()
        {
            Token token = this.tokenizer.NextToken();

            if (token == null)
                return null;

            if (token.TokenType == TokenType.DotsName)
                return (string)token.Value;

            this.tokenizer.PushToken(token);

            return null;
        }

        private void CompileToken(string value)
        {
            Token token = this.tokenizer.NextToken();

            if (token == null || token.Value == null || !value.Equals(token.Value))
                throw new ParserException(string.Format(CultureInfo.CurrentCulture, "Expected {0}", value));
        }

        private IExpression CompileCall(string name)
        {
            IProcedure procedure = this.machine.GetProcedure(name);

            if (procedure == null)
                throw new ParserException(string.Format(CultureInfo.CurrentUICulture, "Unknown procedure {0}", name));

            List<IExpression> parameters = new List<IExpression>();

            if (procedure.Arity == -1)
            {
                IExpression expression = this.CompileSimpleExpression();

                while (expression != null)
                {
                    parameters.Add(expression);
                    expression = this.CompileSimpleExpression();
                }
            }
            else
            {
                for (int k = 0; k < procedure.Arity; k++)
                    parameters.Add(this.CompileExpression());
            }

            return new CallProcedure(procedure, parameters);
        }
    }
}

